## Security_Bot 1.0
##-----------------------------------------------------------------
## Announces Lastest Security News Through RSS Feeds
##
##
##This bot will check the rss xml files upon request or autochecks then it will post the title and link to the news article online.
##It then makes two files containing the rss feed information, so it doesn't spam the chat when it opens again.
##
##Copyright (c) 2011 Ben Virgilio <nebriv@gmail.com>
##
##    Permission is hereby granted, free of charge, to any person obtaining
##a copy of this software and associated documentation files (the
##"Software"), to deal in the Software without restriction, including
##without limitation the rights to use, copy, modify, merge, publish,
##distribute, sublicense, and/or sell copies of the Software, and to
##permit persons to whom the Software is furnished to do so, subject to
##the following conditions:
##
##    The above copyright notice and this permission notice shall be
##included in all copies or substantial portions of the Software. The
##creator, as stated in the copyright line, is notified of any major changes (branches off, etc)
##via email and, if possible, given a copy.
##
##    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
##EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
##MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
##IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
##CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
##TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
##SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
#
#-----------------------------------------------------------------
#
#Imports
#===============
import ConfigParser
import socket
import urllib2
from xml.dom import minidom
import time
import pickle
import untangle
import fnmatch
import os
import datetime as dt
from time import strftime as date
#Vars
#===============
global on, restart
restart = 0
on=1
fname='config.ini'
config=ConfigParser.ConfigParser()
config.read(fname)
sec_list=[]
op_list=[]
pingcount = 0 #Reseting pingcount to 0, var is used to count the number of pings from server
updateforce = 0 #A variable so the bot outputs something when a user forces an update
articlesince = 0 #counts the number of articles since someone last requested them
inactivechat = 0
today = dt.datetime.now()
today = today.strftime("%y-%m-%d")

#Functions
#===============

#   Function: config_load
#Description: Loads the config file from the fname variable and sets all variables in the file as global
#  Variables: None.
#    Returns: None.

def config_load():
    print "Loading config file..."
    for section in config.sections():
        sec_list.append(section)
        for option in config.options(section):
            try:
                value = config.getint(section, option)
            except ValueError:
                value = config.get(section,option)

##            if value.isdigit():
##                globals() [option]=int(value)
##            else:
##                globals() [option]=value
            globals() [option]=value
            op_list.append(option)
        del option
    del section
            
##            if option == "verbose" or option == "logging" or option == "pings" or option == "autocheck" or option == "buffer":
##                globals()[option]=config.getint(section,option)
##                op_list.append(option)
##            else:
##            value = config.get(section,option)
##            if value.isdigit():
##                value = int(config.get(section,option)
##            globals() [option]=int(config.get(section,option)
##            
##                value = config.get(section,option)
##            globals()[option]=value
##            op_list.append(option)

#   Function: config_save
#Description: Saves the global variables, if its the verbose variable, it sets it to be a string 
#  Variables: None
#    Returns: None
#
def config_save():
    for section in config.sections():
        for option in config.options(section):
            if option.isdigit():
                config.set(section, option, str(globals()[option]))
            else:
                config.set(section, option, globals()[option])
            with open('config.ini', 'w') as configfile:
                config.write(configfile)

#   Function: load_files
#Description: Loads the files used to store rss feed information. If the file exists, it loads the info into an array
#                 if the file doesn't exist, it creates a new array.
#  Variables: None
#    Returns: None
#
def load_files():
    global feeds, link, title
    try:
        if verbose:
            print "Loading link file...."
        link = pickle.load( open( "link", "rb" ) )
    except IOError as e:
        if verbose:
            print "Link File Doesn't Exist"
        link = []
    try: 
        if verbose:
            print "Loading title file...."
        title = pickle.load( open( "title", "rb" ) )
    except IOError as e:
        if verbose:
            print "Title File Doesn't Exist"
        title = []
    try:
        if verbose:
            print "Loading feed file...."
        feeds = pickle.load( open( "feeds", "rb") )
    except IOError as e:
        if verbose:
            print "Feeds File Doesn't Exist"
        feeds = ['http://threatpost.com/en_us/rss.xml']
                
#   Function: hello() 
#Description: Sends "hello"
#  Variables: None
#    Returns: None
#
def hello():
    global pings, autocheck
    sendmsg("Hello!")
    if verbose:
        print "Hello!"

#   Function: entrancemsg() 
#Description: Displays the message when called. THIS SHOULD BE CHANGED TO USE THE CONFIG FILE
#  Variables: None
#    Returns: None
#
def entrancemsg():
    sendmsg("Have no fear! "+nick+" is here!")
    time.sleep(1)
    sendmsg("For available commands type !help")

#   Function: verbosity()
#Description: Changes the verbosity level variable and saves the config file. Then it announces the level.
#  Variables: Level - the level of verbosity or "get level"
#    Returns: None.
#
def verbosity(level):
    global verbose
    if level == 0:
        verbose = 0
        config_save()
        sendmsg("Verbosity level changed to "+str(verbose))
        print "Verbosity level changed to "+str(verbose)
    elif level == 1:
        verbose = 1
        config_save()
        sendmsg("Verbosity level changed to "+str(verbose))
        print "Verbosity level changed to "+str(verbose)
    elif level == 2:
        verbose = 2
        config_save()
        sendmsg("Verbosity level changed to "+str(level))
        print "Verbosity level changed to "+str(verbose)
    elif level == "getlevel":
        sendmsg("Verbosity is at level: "+str(verbose))
        print "Verbosity is at level: "+str(verbose)   

#   Function: changenick() 
#Description: Changes the bot's nick. Only currently used when the bot encounters a ghost of it's self . 
#  Variables: thenick
#    Returns: 

def changenick(thenick):
    
    ircsock.send("NICK :"+thenick+"\n")

#   Function: action() 
#Description: Performs an action (like the /me command)...NOT YET COMPLETED.
#  Variables: theaction
#    Returns: 

def action(theaction):
    sendmsg("/me "+theaction)

#   Function: search() 
#Description: Performs a keyword search on all the titles in the title array. Decides how quickly to send the 
#                  messages based on how many articles are found. ONLY SENDS UP TO 10 MESSAGES.
#  Variables: arttitle
#    Returns: 

def search(arttitle):
    matches = []
    for atitle in title:
        if fnmatch.fnmatch(atitle, '*'+arttitle+'*'):
            matches.append(atitle)
    if len(matches) > 10:
        sendmsg("First 10 Matches:")
        sendmsg("======================")
        a = 0
        for i in range(len(matches)):
            if a < 10:
                sendmsg(matches[i].encode('ascii', 'ignore'))
                linknumber = title.index(matches[i])
                sendmsg(link[linknumber].encode('ascii', 'ignore'))
                sendmsg("---------")
                time.sleep(3)
                a += 1
    elif matches:
        if len(matches) > 5:
            sleepamount = 3
        else:
            sleepamount = 1.5
        sendmsg(str(len(matches))+" Matches:")
        sendmsg("======================")
        for i in range(len(matches)):
                sendmsg(matches[i].encode('ascii', 'ignore'))
                linknumber = title.index(matches[i])
                sendmsg(link[linknumber].encode('ascii', 'ignore'))
                sendmsg("---------")
                time.sleep(sleepamount)
    else:
        sendmsg("No Matches")

#   Function: ping() 
#Description: Replies to the server's pings. It also keeps a ping count and runs updaterss() if the count is equal to 
#                   the variable "ping" (set in the config)
#  Variables: None
#    Returns: None

def ping():
    global pingcount, pings, autocheck
    pingcount += 1
    if verbose == 1:
        print pingcount
    elif verbose == 2:
        print pingcount
        sendmsg("The pingcount is "+str(pingcount))
    ircsock.send("PONG :Pong\n") #Reply to the server's ping
    if (pingcount == pings) and autocheck: #If 4 pings have passed and the bot autochecks for updates, update
        updaterss()

#   Function: sendmsg() 
#Description: Sends a channel message to the irc chat. Used by many other functions.
#  Variables: msg - contains the message to be sent
#    Returns: None

def sendmsg(msg): #Function to send a message to the channel
    try:
        ircsock.send("PRIVMSG "+ channel +" :"+ msg +"\n")
        time2 = date('%H:%M:%S')
        data = time2+" - "+nick+": "+msg+"\r\n"
        log(data)
    finally:
        if verbose:
            print "There was an error sending the message"
#   Function: sendnotice()
#Description: Sends a notice to the user...(only displays the message to the specific user)
#  Variables: usernick-the nick to send the notice to, msg-the message to send
#    Returns: None
def sendnotice(usernick, msg):
    ircsock.send("NOTICE "+ usernick +" :"+ msg +"\n")

#   Function: joinchan() 
#Description: Sends the IRC JOIN command.
#  Variables: chan-the channel which should be joined
#    Returns: 
def joinchan(chan): #Function to join a channel
    ircsock.send("JOIN "+ chan +"\n")

#   Function: writefile()
#Description: Writes a file containing one of three arrays.
#  Variables: name-the file which will be saved, array-the file to be saved.
#    Returns: None
def writefile(name, array): #Writes the array to file
    pickle.dump(array, open( name, "wb"))

#   Function: notauthorized()
#Description: Function that runs if an admin command is issued by a non-admin
#  Variables: None
#    Returns: None
def notauthorized():
    sendmsg("Sorry, you are not authoized to use that command.")

#   Function: log()
#Description: Writes to a log file, if logging is enabled.
#  Variables: Data-the data to be written to the log.
#    Returns: None
def log(data):
    global logging
    if logging == 1:
        try:
            file = open("logs/"+today+".txt", 'a')
        except IOError as e:
            if verbose:
                print "Making logs folder..."
                os.mkdir("logs")    
                file = open("logs/"+today+".txt", 'a')
        file.write(data)
        file.close

#   Function: shutdown()
#Description: Saves the arrays and config file, then sends the quit command to IRC then exit()s python
#  Variables: None
#    Returns: None
def shutdown():
    leave()
    time.sleep(1)
    exit()

#   Function: leave()
#Description: Same as shutdown() except it doesn't exit python.
#  Variables: None
#    Returns: None
def leave():
    writefile("link", link)   #write the arrays to file, saves the rss arrays already downloaded
    writefile("feeds", feeds)
    writefile("title", title) #so it doesn't spam chat on startup, function defined below.
    config_save()
    if verbose:
        print("Leave Command")
    ircsock.send("PART "+channel+" : "+owner+" told me to leave =( \n")

#   Function: updaterss()
#Description: Fairly complex function.... #######COMMENT THIS FUNCTION#############
#  Variables: None
#    Returns: None
def updaterss():
    global feeds, updateforce, articlesince, pingcount, inactivechat, verbose
    pingcount = 0 #Resets counter
    if verbose == 2:
        sendmsg("Checking the latest security news!")
        print("Checking the latest news")
    elif verbose:
        print("Checking the latest news")
    for feed in feeds: 
        #try:
        o = untangle.parse(feed)
        for item in o.rss.channel.item:
            title_data = item.title.cdata
            link_data = item.link.cdata
            if title_data.lower() not in [x.lower() for x in title] and link_data not in link:
                print title_data.encode('ascii', 'ignore')
                title.insert(0, title_data)
                print link_data.encode('ascii', 'ignore')
                link.insert(0, link_data)
                articlesince += 1  
                writefile("link", link)   #write the arrays to file, saves the rss arrays already
                writefile("title", title) #downloaded so it doesn't spam chat on startup
    if articlesince and inactivechat and verbose:
        sendmsg("There is a new news article!")
        updateforce = 0
        inactivechat = None
    elif articlesince and inactivechat is None:
        print "There is new news, however the chat is inactive"
    elif articlesince and updateforce:
        sendmsg("There is a new news article")
        if verbose:
            print "There is a new news article"
        updateforce = 0
    elif (articlesince < 1) and updateforce:
        sendmsg("There is no news =(")
        if verbose:
            print "Update was forced, but there is no news"
        updateforce = 0
    else:
        if verbose == 2:
            sendmsg("There is no new news.")
        if verbose:
            print "There is no new news."

#   Function: getnews()
#Description: Prints the newest articles found by updaterss as well as prints a number of articles
#  Variables: number - specified by user in chat, if its none it'll be called latest and get the latest news
#    Returns: None
def getnews(number):
    global articlesince, on, verbose
    if number.isdigit():
        number = int(number)
        if number > 10:
            sleepamount = 6
        elif number > 3:
            sleepamount = 3
        else:
            sleepamount = 1
        if verbose:
            print ""
        for entry in range(number):
            ircsock.send("PRIVMSG "+ channel +" :"+title[entry].encode('ascii', 'ignore')+"\n")
            ircsock.send("PRIVMSG "+ channel +" :"+link[entry].encode('ascii', 'ignore')+"\n")
            sendmsg("-----------------------")
            if verbose:
                print title[entry]
                print link[entry]
                print ""
            time.sleep(sleepamount)
        time.sleep(5)
        on = 1
    elif number == "latest": 
        if articlesince: #articlesince increases by one for every article found by updaterss()
            if articlesince > 10: #checks how many articles updaterss found.
                sleepamount = 3
            elif articlesince > 5:
                sleepamount = 2
            else:
                sleepamount = 1
            for entry in range(articlesince): 
                sendmsg(title[entry].encode('ascii', 'ignore'))
                sendmsg(link[entry].encode('ascii', 'ignore'))
                sendmsg("-----------------------")
                if verbose:
                    print title[entry]
                    print link[entry]
                    print ""
                time.sleep(sleepamount)
            articlesince = 0
            time.sleep(3)
            on = 1
        else:
            sendmsg("No new news to print =(")
            if verbose:
                print "No new news"
            time.sleep(1)
            on = 1


def about(usernick):
    global on
    sendnotice(usernick, nick+" Version "+str(version))
    sendnotice(usernick, "======================")
    sendnotice(usernick, nick+" Announces Lastest Security News by Reading RSS Feeds-made by Ben Virgilio.")
    sendnotice(usernick, "http://code.google.com/p/irc-security-bot/")
    sendnotice(usernick, "-----------------------")
    sendnotice(usernick, "If you'd like to add another feed, ask nebriv")
    time.sleep(1.5)
    on = 1
    
def help(usernick):
    global on
    sendnotice(usernick, "Available Commands")
    sendnotice(usernick, "======================")
    sendnotice(usernick, " !update, forces bot to check news")
    sendnotice(usernick, " !checknew, checks if there are new articles downloaded")
    sendnotice(usernick, " !news, output desired number of the most recent articles. ex. !news 3")
    sendnotice(usernick, " !listfeeds, list the RSS feeds the bot pulls data from.")
    sendnotice(usernick, " !search <keyword>, searches all the articles for a keyword (newest first-can only return up to 10 matches)")
    sendnotice(usernick, " !about, about the bot")
    time.sleep(1.5)
    on = 1

def checknew():
    if articlesince:
        sendmsg("There are "+str(articlesince)+" new articles! Type !latest to read them!")
    else:
        sendmsg("There are no new articles!")

def addfeed(feed):
    feeds.append(feed)
    writefile("feeds", feeds)
    sendmsg("Added "+feed+" to the feeds.")
    if verbose:
        print "Added a new feed"

def removefeed(feed):
    if feed in feeds:
        feeds.remove(feed)
        writefile("feeds", feeds)
        sendmsg("Removed "+feed+" from the feeds.")
        if verbose:
            print "Removed a feed"
    else:
        sendmsg("That feed doesn't exist!")

def listfeeds():
    for feed in feeds:
        if verbose:
            print feed
        sendmsg(feed)

def removearticle(arttitle):
    if arttitle in title:
        link.pop(title.index(arttitle))
        title.pop(title.index(arttitle))
        writefile("title", title)
        writefile("link", link)
        sendmsg("Removed "+arttitle+" from the articles!")
        if verbose:
            print "Removed a article"
    else:
        sendmsg("That article doesn't exist!")

#Commands/ACTUALLY RUNNING STUFF
#===============
def run():
    config_load()
    load_files()
    global ircsock, on, restart, verbose, logging, updateforce
    ircsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    ircsock.connect((server, 6667))
    ircsock.send("USER "+ nick +" "+ nick +" "+ nick +" :Nebriv's python security bot\n")
    ircsock.send("NICK "+ nick +"\n")
    currentserver = ircsock.recv(int(buffer))
    currentserver = currentserver[currentserver.find(":")+1:currentserver.find(" NOTICE *")]
    time.sleep(1)
    joinchan(channel)
    entrancemsg()
    while on:
        ircmsg = ircsock.recv(int(buffer))
        ircmsg = ircmsg.strip('\n\r')
        usernick=ircmsg[ircmsg.find(":")+1:ircmsg.find("!")]
        if verbose:
            print(ircmsg)
        if fnmatch.fnmatch(ircmsg, "PING :"+currentserver):
            ping()
        elif fnmatch.fnmatch(ircmsg, ":"+currentserver+" 433"+'*'):
            if verbose:
                print("Nick is already in use, initiating alternate nick...")
            ircsock.send("PRIVMSG NICKSERV :GHOST "+nick+" "+nickpass+"\n")
            ircsock.send("USER "+ nick2 +" "+ nick2 +" "+ nick2 +" :Nebriv's python security bot\n")
            ircsock.send("NICK "+ nick2 +"\n")
            changenick(nick)
            joinchan(channel)
            time.sleep(2)
        elif fnmatch.fnmatch(ircmsg, ":NickServ!NickServ@services. NOTICE Security_Bot :This nickname is registered."+'*'):
            ircsock.send("PRIVMSG NICKSERV :IDENTIFY "+nick+" "+nickpass+"\n")
        else:
            if ircmsg.find("PRIVMSG "+channel) != -1:
                inactivechat = 1
                themsg = ircmsg.partition("PRIVMSG "+channel+" :")
                if isinstance(themsg[2], str):
                    
                    time2 = date('%H:%M:%S')
                    data = time2+" - "+usernick+": "+themsg[2]+"\r\n"
                    log(data)
                if verbose:
                    print "Chat is active!"
        #--------------------
        #LISTEN FOR SECTION:#
        #-------------------
        #Modify the below commands if you added additional commands
        # or if you want to change the commands that the bot listens for.
        #
            if ircmsg.find(":!shutdown") != -1:
                if ircmsg.find(ownerhostmask) and usernick == owner:
                    shutdown()
                else:
                    notauthorized()
            if ircmsg.find(":!leave") != -1:
                if ircmsg.find(ownerhostmask) and usernick == owner:
                    leave()
                else:
                    notauthorized()
            if ircmsg.find(":!reconnect") != -1:
                if ircmsg.find(ownerhostmask) and usernick == owner:
                    leave()
                    restart = 1
                    on = 0
                else:
                    notauthorized()
            if ircmsg.find(":!addfeed") != -1:
                if ircmsg.find(ownerhostmask) and usernick == owner:
                    feed = ircmsg.partition(":!addfeed ")
                    if isinstance(feed[2], str):
                        addfeed(feed[2])
                else:
                    notauthorized()

            if ircmsg.find(":!removefeed") != -1:
                if ircmsg.find(ownerhostmask) and usernick == owner:
                    feed = ircmsg.partition(":!removefeed ")
                    if isinstance(feed[2], str):
                        removefeed(feed[2])
                else:
                    notauthorized()
                    
            if ircmsg.find(":!verbosity") != -1:
                if ircmsg.find(ownerhostmask) and usernick == owner:
                    level = ircmsg.partition(":!verbosity ")
                    if level[2].isdigit():
                        verbosity(int(level[2]))
                    else:
                        verbosity("getlevel")
                else:
                    notauthorized()

            if ircmsg.find(":!logging") != -1:
                if ircmsg.find(ownerhostmask) and usernick == owner:
                    loggingvar = ircmsg.partition(":!logging ")
                    if loggingvar[2] == "off" or loggingvar[2] == "Off":
                        logging = 0
                        sendmsg("I am no longer logging this chat")
                    elif loggingvar[2] == "on" or loggingvar[2] == "On":
                        logging = 1
                        sendmsg("I am now logging this chat")
                    else:
                        if logging == 1:
                            sendmsg("I am currently logging this chat")
                        else:
                            sendmsg("I am not logging this chat")
                else:
                    notauthorized()

            if ircmsg.find(":!removearticle") != -1:
                if ircmsg.find(ownerhostmask) and usernick == owner:
                    arttitle = ircmsg.partition(":!removearticle ")
                    if arttitle[2] and isinstance(arttitle[2], str):
                        removearticle(arttitle[2])
                    else:
                        sendmsg("Specify an article!")
                else:
                    notauthorized()
##            else:
##                    sendmsg("You are not authorized to execute that command")
            if ircmsg.find(":Hello " + nick) != -1:
                hello()

            if ircmsg.find(":!update") != -1:
                updateforce = 1
                updaterss()
                
            if ircmsg.find(":!news") != -1:
                news = ircmsg.partition(":!news ")
                if news[2].isdigit():
                    on = 0
                    getnews(news[2])
                else:
                    on = 0
                    getnews("latest")

            if ircmsg.find(":!help") != -1:
                usernick=ircmsg[ircmsg.find(":")+1:ircmsg.find("!")]
                on = 0
                help(usernick)

            if ircmsg.find(":!about") != -1:
                usernick=ircmsg[ircmsg.find(":")+1:ircmsg.find("!")]
                on = 0
                about(usernick)
            
            if ircmsg.find(":!checknew") != -1:
                checknew()

            if ircmsg.find(":!listfeeds") != -1:
                listfeeds()

            if ircmsg.find(":!search") != -1:
                searchtitle = ircmsg.partition(":!search ")
                if searchtitle[2] and isinstance(searchtitle[2], str):
                    search(searchtitle[2])
                else:
                    sendmsg("Invalid input! Type !help for assitance")

            if ircmsg.find(":!action") != -1:
                myaction = ircmsg.partition(":!action ")
                action(myaction[2])

while on or restart:
    run()
    if restart:
        on = 1
